---
title: "MCP-UI"
description: "Build interactive UI widgets with MCP-UI framework"
icon: "wallpaper"
---

MCP-UI Resources enable you to build rich, interactive user interfaces that work seamlessly with MCP servers. These widgets can be embedded in MCP-compatible clients to provide visual interfaces alongside your tools and resources.

MCP-UI resources follow the proposed MCP-UI specification [here](https://github.com/idosal/mcp-ui/blob/main/SPEC.md).

<Warning>
The MCP-UI resources are not directly related to the Apps SDK widgets, we recommend using the [Apps SDK widgets](/typescript/server/ui-widgets) instead.
</Warning>

## Resource Types

### 1. External URL Resources

Iframe-based widgets served from your MCP server

Check an example here [mcp-ui-example](https://github.com/idosal/mcp-ui/tree/main/examples/server/mcp-ui)


```typescript
server.uiResource({
  type: 'externalUrl',
  name: 'dashboard',
  widget: 'analytics-dashboard',
  title: 'Analytics Dashboard',
  description: 'Real-time analytics visualization',
  props: {
    timeRange: {
      type: 'string',
      description: 'Time range for data',
      required: false,
      default: '7d'
    },
    metric: {
      type: 'string',
      description: 'Metric to display',
      required: false,
      default: 'revenue'
    }
  },
  size: ['800px', '600px'],
  annotations: {
    audience: ['user'],
    priority: 0.9
  }
})
```

**Characteristics:**
- Served as standalone HTML pages
- Isolated in iframes for security
- Can include external resources
- Full JavaScript capabilities

<Tip>
External URLs are automatically built and configured by the `setupWidgetRoutes` function in `mcp-use/server`. Routes are generated based on your widget definitions without manual setup.
</Tip>

### 2. Raw HTML Resources

Inline HTML content rendered directly:

Check an example here [mcp-ui-example](https://github.com/mcp-use/mcp-use/blob/main/libraries/typescript/packages/create-mcp-use-app/src/templates/mcp-ui/index.ts#L38)

```typescript
server.uiResource({
  type: 'rawHtml',
  name: 'simple_form',
  title: 'Contact Form',
  description: 'Simple contact form',
  htmlString: `
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        body {
          font-family: -apple-system, sans-serif;
          padding: 20px;
        }
        .form-group {
          margin-bottom: 15px;
        }
        input, textarea {
          width: 100%;
          padding: 8px;
          border: 1px solid #ddd;
          border-radius: 4px;
        }
        button {
          background: #007bff;
          color: white;
          padding: 10px 20px;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
      </style>
    </head>
    <body>
      <h2>Contact Us</h2>
      <form id="contactForm">
        <div class="form-group">
          <input type="text" placeholder="Name" required>
        </div>
        <div class="form-group">
          <input type="email" placeholder="Email" required>
        </div>
        <div class="form-group">
          <textarea placeholder="Message" rows="5" required></textarea>
        </div>
        <button type="submit">Send Message</button>
      </form>
      <script>
        document.getElementById('contactForm').onsubmit = (e) => {
          e.preventDefault();
          alert('Message sent successfully!');
        };
      </script>
    </body>
    </html>
  `,
  size: ['400px', '500px']
})
```

**Characteristics:**
- Renders inline without iframe
- Simpler but less isolated
- Good for basic interactions
- Limited external resource loading

### 3. Remote DOM Resources

JavaScript-driven dynamic interfaces:

Check an example here [mcp-ui-example](https://github.com/mcp-use/mcp-use/blob/main/libraries/typescript/packages/create-mcp-use-app/src/templates/mcp-ui/index.ts#L136)

```typescript
server.uiResource({
  type: 'remoteDom',
  name: 'interactive_chart',
  title: 'Interactive Chart',
  description: 'Dynamic data visualization',
  remoteDomFramework: 'react',
  remoteDomCode: `
    function ChartWidget() {
      const [data, setData] = React.useState([]);
      const [loading, setLoading] = React.useState(true);

      React.useEffect(() => {
        // Fetch data from MCP server
        fetch('/api/chart-data')
          .then(res => res.json())
          .then(data => {
            setData(data);
            setLoading(false);
          });
      }, []);

      if (loading) {
        return <div>Loading chart data...</div>;
      }

      return (
        <div style={{ padding: '20px' }}>
          <h2>Sales Dashboard</h2>
          <div className="chart-container">
            {data.map(item => (
              <div key={item.id} style={{
                height: item.value + 'px',
                width: '50px',
                background: '#007bff',
                display: 'inline-block',
                margin: '0 5px'
              }}>
                <span>{item.label}</span>
              </div>
            ))}
          </div>
        </div>
      );
    }

    ReactDOM.render(<ChartWidget />, document.getElementById('root'));
  `,
  props: {
    refreshInterval: {
      type: 'number',
      description: 'Refresh interval in seconds',
      default: 60
    }
  }
})
```

**Characteristics:**
- Dynamic JavaScript execution
- React/Vue/vanilla JS support
- Real-time updates possible
- More complex interactions

## Building React Widgets

### Project Structure

```
my-mcp-server/
├── resources/
│   ├── kanban-board.tsx
├── src/
   └── server.ts
```


### Example: Kanban Board Widget

```tsx
// resources/kanban-board.tsx
import React, { useState, useEffect } from 'react'
import './kanban-board.css'

interface Task {
  id: string
  title: string
  description: string
  status: 'todo' | 'in-progress' | 'done'
  priority: 'low' | 'medium' | 'high'
  assignee?: string
}

export default function KanbanBoard() {
  const [tasks, setTasks] = useState<Task[]>([])
  const [draggedTask, setDraggedTask] = useState<string | null>(null)

  // Parse URL parameters
  useEffect(() => {
    const params = new URLSearchParams(window.location.search)
    const initialTasks = params.get('tasks')
    if (initialTasks) {
      try {
        setTasks(JSON.parse(initialTasks))
      } catch (e) {
        console.error('Failed to parse initial tasks')
      }
    }
  }, [])

  const handleDragStart = (taskId: string) => {
    setDraggedTask(taskId)
  }

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault()
  }

  const handleDrop = (e: React.DragEvent, newStatus: Task['status']) => {
    e.preventDefault()
    if (!draggedTask) return

    setTasks(tasks.map(task =>
      task.id === draggedTask
        ? { ...task, status: newStatus }
        : task
    ))
    setDraggedTask(null)
  }

  const columns: { status: Task['status']; title: string }[] = [
    { status: 'todo', title: 'To Do' },
    { status: 'in-progress', title: 'In Progress' },
    { status: 'done', title: 'Done' }
  ]

  return (
    <div className="kanban-board">
      <h1>Project Tasks</h1>
      <div className="columns">
        {columns.map(column => (
          <div
            key={column.status}
            className="column"
            onDragOver={handleDragOver}
            onDrop={(e) => handleDrop(e, column.status)}
          >
            <h2>{column.title}</h2>
            <div className="tasks">
              {tasks
                .filter(task => task.status === column.status)
                .map(task => (
                  <div
                    key={task.id}
                    className={`task priority-${task.priority}`}
                    draggable
                    onDragStart={() => handleDragStart(task.id)}
                  >
                    <h3>{task.title}</h3>
                    <p>{task.description}</p>
                    {task.assignee && (
                      <span className="assignee">{task.assignee}</span>
                    )}
                  </div>
                ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}
```

### Widget Styling

```css
/* resources/kanban-board.css */
.kanban-board {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  padding: 20px;
  background: #f5f5f5;
  min-height: 100vh;
}

.columns {
  display: flex;
  gap: 20px;
  margin-top: 20px;
}

.column {
  flex: 1;
  background: white;
  border-radius: 8px;
  padding: 15px;
  min-height: 400px;
}

.column h2 {
  margin: 0 0 15px 0;
  font-size: 18px;
  color: #333;
}

.task {
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  padding: 12px;
  margin-bottom: 10px;
  cursor: move;
  transition: transform 0.2s;
}

.task:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

.task.priority-high {
  border-left: 4px solid #f44336;
}

.task.priority-medium {
  border-left: 4px solid #ff9800;
}

.task.priority-low {
  border-left: 4px solid #4caf50;
}

.assignee {
  display: inline-block;
  background: #e3f2fd;
  color: #1976d2;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  margin-top: 8px;
}
```


## Next Steps

- [UI Widgets](./ui-widgets) - General widget guide
- [Templates](./templates) - Project templates overview
- [Examples](./examples) - Complete implementations